/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    Copyright (C) 2004
 *    & Matthias Schubert (schubert@dbs.ifi.lmu.de)
 *    & Zhanna Melnikova-Albrecht (melnikov@cip.ifi.lmu.de)
 *    & Rainer Holzmann (holzmann@cip.ifi.lmu.de) 
 */

package weka.clusterers;

import weka.clusterers.forOPTICSAndDBScan.DataObjects.DataObject;
import weka.clusterers.forOPTICSAndDBScan.Databases.Database;
import weka.clusterers.forOPTICSAndDBScan.OPTICS_GUI.OPTICS_Visualizer;
import weka.clusterers.forOPTICSAndDBScan.OPTICS_GUI.SERObject;
import weka.clusterers.forOPTICSAndDBScan.Utils.EpsilonRange_ListElement;
import weka.clusterers.forOPTICSAndDBScan.Utils.UpdateQueue;
import weka.clusterers.forOPTICSAndDBScan.Utils.UpdateQueueElement;
import weka.core.Capabilities;
import weka.core.FastVector;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.RevisionUtils;
import weka.core.TechnicalInformation;
import weka.core.TechnicalInformationHandler;
import weka.core.Utils;
import weka.core.Capabilities.Capability;
import weka.core.TechnicalInformation.Field;
import weka.core.TechnicalInformation.Type;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.ReplaceMissingValues;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

/**
 * <!-- globalinfo-start --> Mihael Ankerst, Markus M. Breunig, Hans-Peter
 * Kriegel, Joerg Sander: OPTICS: Ordering Points To Identify the Clustering
 * Structure. In: ACM SIGMOD International Conference on Management of Data,
 * 49-60, 1999.
 * <p/>
 * <!-- globalinfo-end -->
 * 
 * <!-- technical-bibtex-start --> BibTeX:
 * 
 * <pre>
 * &#64;inproceedings{Ankerst1999,
 *    author = {Mihael Ankerst and Markus M. Breunig and Hans-Peter Kriegel and Joerg Sander},
 *    booktitle = {ACM SIGMOD International Conference on Management of Data},
 *    pages = {49-60},
 *    publisher = {ACM Press},
 *    title = {OPTICS: Ordering Points To Identify the Clustering Structure},
 *    year = {1999}
 * }
 * </pre>
 * <p/>
 * <!-- technical-bibtex-end -->
 * 
 * <!-- options-start --> Valid options are:
 * <p/>
 * 
 * <pre>
 * -E &lt;double&gt;
 *  epsilon (default = 0.9)
 * </pre>
 * 
 * <pre>
 * -M &lt;int&gt;
 *  minPoints (default = 6)
 * </pre>
 * 
 * <pre>
 * -I &lt;String&gt;
 *  index (database) used for OPTICS (default = weka.clusterers.forOPTICSAndDBScan.Databases.SequentialDatabase)
 * </pre>
 * 
 * <pre>
 * -D &lt;String&gt;
 *  distance-type (default = weka.clusterers.forOPTICSAndDBScan.DataObjects.EuclidianDataObject)
 * </pre>
 * 
 * <pre>
 * -F
 *  write results to OPTICS_#TimeStamp#.TXT - File
 * </pre>
 * 
 * <pre>
 * -no-gui
 *  suppress the display of the GUI after building the clusterer
 * </pre>
 * 
 * <pre>
 * -db-output &lt;file&gt;
 *  The file to save the generated database to. If a directory
 *  is provided, the database doesn't get saved.
 *  The generated file can be viewed with the OPTICS Visualizer:
 *    java weka.clusterers.forOPTICSAndDBScan.OPTICS_GUI.OPTICS_Visualizer [file.ser]
 *  (default: .)
 * </pre>
 * 
 * <!-- options-end -->
 * 
 * @author Matthias Schubert (schubert@dbs.ifi.lmu.de)
 * @author Zhanna Melnikova-Albrecht (melnikov@cip.ifi.lmu.de)
 * @author Rainer Holzmann (holzmann@cip.ifi.lmu.de)
 * @version $Revision: 5538 $
 */
public class OPTICS extends AbstractClusterer implements OptionHandler,
		TechnicalInformationHandler {

	/** for serialization */
	static final long serialVersionUID = 274552680222105221L;

	/**
	 * Specifies the radius for a range-query
	 */
	private double epsilon = 0.9;

	/**
	 * Specifies the density (the range-query must contain at least minPoints
	 * DataObjects)
	 */
	private int minPoints = 6;

	/**
	 * Replace missing values in training instances
	 */
	private ReplaceMissingValues replaceMissingValues_Filter;

	/**
	 * Holds the number of clusters generated
	 */
	private int numberOfGeneratedClusters;

	/**
	 * Holds the distance-type that is used (default =
	 * weka.clusterers.forOPTICSAndDBScan.DataObjects.EuclidianDataObject)
	 */
	private String database_distanceType = "weka.clusterers.forOPTICSAndDBScan.DataObjects.EuclidianDataObject";

	/**
	 * Holds the type of the used database (default =
	 * weka.clusterers.forOPTICSAndDBScan.Databases.SequentialDatabase)
	 */
	private String database_Type = "weka.clusterers.forOPTICSAndDBScan.Databases.SequentialDatabase";

	/**
	 * The database that is used for OPTICS
	 */
	private Database database;

	/**
	 * Holds the time-value (seconds) for the duration of the clustering-process
	 */
	private double elapsedTime;

	/**
	 * Flag that indicates if the results are written to a file or not
	 */
	private boolean writeOPTICSresults = false;

	/**
	 * Holds the ClusterOrder (dataObjects with their r_dist and c_dist) for the
	 * GUI
	 */
	private FastVector resultVector;

	/** whether to display the GUI after building the clusterer or not. */
	private boolean showGUI = true;

	/** the file to save the generated database object to. */
	private File databaseOutput = new File(".");

	// *****************************************************************************************************************
	// constructors
	// *****************************************************************************************************************

	// *****************************************************************************************************************
	// methods
	// *****************************************************************************************************************

	/**
	 * Returns default capabilities of the clusterer.
	 * 
	 * @return the capabilities of this clusterer
	 */
	public Capabilities getCapabilities() {
		Capabilities result = super.getCapabilities();
		result.disableAll();
		result.enable(Capability.NO_CLASS);

		// attributes
		result.enable(Capability.NOMINAL_ATTRIBUTES);
		result.enable(Capability.NUMERIC_ATTRIBUTES);
		result.enable(Capability.DATE_ATTRIBUTES);
		result.enable(Capability.MISSING_VALUES);

		return result;
	}

	/**
	 * Generate Clustering via OPTICS
	 * 
	 * @param instances
	 *            The instances that need to be clustered
	 * @throws java.lang.Exception
	 *             If clustering was not successful
	 */
	public void buildClusterer(Instances instances) throws Exception {
		// can clusterer handle the data?
		getCapabilities().testWithFail(instances);

		resultVector = new FastVector();
		long time_1 = System.currentTimeMillis();

		numberOfGeneratedClusters = 0;

		replaceMissingValues_Filter = new ReplaceMissingValues();
		replaceMissingValues_Filter.setInputFormat(instances);
		Instances filteredInstances = Filter.useFilter(instances,
				replaceMissingValues_Filter);

		database = databaseForName(getDatabase_Type(), filteredInstances);
		for (int i = 0; i < database.getInstances().numInstances(); i++) {
			DataObject dataObject = dataObjectForName(
					getDatabase_distanceType(), database.getInstances()
							.instance(i), Integer.toString(i), database);
			database.insert(dataObject);
		}
		database.setMinMaxValues();

		UpdateQueue seeds = new UpdateQueue();

		/** OPTICS-Begin */
		Iterator iterator = database.dataObjectIterator();
		while (iterator.hasNext()) {
			DataObject dataObject = (DataObject) iterator.next();
			if (!dataObject.isProcessed()) {
				expandClusterOrder(dataObject, seeds);
			}
		}

		long time_2 = System.currentTimeMillis();
		elapsedTime = (double) (time_2 - time_1) / 1000.0;

		if (writeOPTICSresults) {
			String fileName = "";
			GregorianCalendar gregorianCalendar = new GregorianCalendar();
			String timeStamp = gregorianCalendar.get(Calendar.DAY_OF_MONTH)
					+ "-" + (gregorianCalendar.get(Calendar.MONTH) + 1) + "-"
					+ gregorianCalendar.get(Calendar.YEAR) + "--"
					+ gregorianCalendar.get(Calendar.HOUR_OF_DAY) + "-"
					+ gregorianCalendar.get(Calendar.MINUTE) + "-"
					+ gregorianCalendar.get(Calendar.SECOND);
			fileName = "OPTICS_" + timeStamp + ".TXT";

			FileWriter fileWriter = new FileWriter(fileName);
			BufferedWriter bufferedOPTICSWriter = new BufferedWriter(fileWriter);
			for (int i = 0; i < resultVector.size(); i++) {
				bufferedOPTICSWriter
						.write(format_dataObject((DataObject) resultVector
								.elementAt(i)));
			}
			bufferedOPTICSWriter.flush();
			bufferedOPTICSWriter.close();
		}

		// explicit file provided to write the generated database to?
		if (!databaseOutput.isDirectory()) {
			try {
				FileOutputStream fos = new FileOutputStream(databaseOutput);
				ObjectOutputStream oos = new ObjectOutputStream(fos);
				oos.writeObject(getSERObject());
				oos.flush();
				oos.close();
				fos.close();
			} catch (Exception e) {
				System.err.println("Error writing generated database to file '"
						+ getDatabaseOutput() + "': " + e);
				e.printStackTrace();
			}
		}

		if (showGUI)
			new OPTICS_Visualizer(getSERObject(),
					"OPTICS Visualizer - Main Window");
	}

	/**
	 * Expands the ClusterOrder for this dataObject
	 * 
	 * @param dataObject
	 *            Start-DataObject
	 * @param seeds
	 *            SeedList that stores dataObjects with reachability-distances
	 */
	private void expandClusterOrder(DataObject dataObject, UpdateQueue seeds) {
		List list = database.coreDistance(getMinPoints(), getEpsilon(),
				dataObject);
		List epsilonRange_List = (List) list.get(1);
		dataObject.setReachabilityDistance(DataObject.UNDEFINED);
		dataObject.setCoreDistance(((Double) list.get(2)).doubleValue());
		dataObject.setProcessed(true);

		resultVector.addElement(dataObject);

		if (dataObject.getCoreDistance() != DataObject.UNDEFINED) {
			update(seeds, epsilonRange_List, dataObject);
			while (seeds.hasNext()) {
				UpdateQueueElement updateQueueElement = seeds.next();
				DataObject currentDataObject = (DataObject) updateQueueElement
						.getObject();
				currentDataObject.setReachabilityDistance(updateQueueElement
						.getPriority());
				List list_1 = database.coreDistance(getMinPoints(),
						getEpsilon(), currentDataObject);
				List epsilonRange_List_1 = (List) list_1.get(1);
				currentDataObject.setCoreDistance(((Double) list_1.get(2))
						.doubleValue());
				currentDataObject.setProcessed(true);

				resultVector.addElement(currentDataObject);

				if (currentDataObject.getCoreDistance() != DataObject.UNDEFINED) {
					update(seeds, epsilonRange_List_1, currentDataObject);
				}
			}
		}
	}

	/**
	 * Wraps the dataObject into a String, that contains the dataObject's key,
	 * the dataObject itself, the coreDistance and its reachabilityDistance in a
	 * formatted manner.
	 * 
	 * @param dataObject
	 *            The dataObject that is wrapped into a formatted string.
	 * @return String Formatted string
	 */
	private String format_dataObject(DataObject dataObject) {
		StringBuffer stringBuffer = new StringBuffer();

		stringBuffer
				.append("("
						+ Utils.doubleToString(
								Double.parseDouble(dataObject.getKey()),
								(Integer.toString(database.size()).length()), 0)
						+ ".) "
						+ Utils.padRight(dataObject.toString(), 40)
						+ "  -->  c_dist: "
						+

						((dataObject.getCoreDistance() == DataObject.UNDEFINED) ? Utils
								.padRight("UNDEFINED", 12)
								: Utils.padRight(Utils.doubleToString(
										dataObject.getCoreDistance(), 2, 3), 12))
						+

						" r_dist: "
						+ ((dataObject.getReachabilityDistance() == DataObject.UNDEFINED) ? Utils
								.padRight("UNDEFINED", 12) : Utils
								.doubleToString(
										dataObject.getReachabilityDistance(),
										2, 3)) + "\n");

		return stringBuffer.toString();
	}

	/**
	 * Updates reachability-distances in the Seeds-List
	 * 
	 * @param seeds
	 *            UpdateQueue that holds DataObjects with their corresponding
	 *            reachability-distances
	 * @param epsilonRange_list
	 *            List of DataObjects that were found in epsilon-range of
	 *            centralObject
	 * @param centralObject
	 */
	private void update(UpdateQueue seeds, List epsilonRange_list,
			DataObject centralObject) {
		double coreDistance = centralObject.getCoreDistance();
		double new_r_dist = DataObject.UNDEFINED;

		for (int i = 0; i < epsilonRange_list.size(); i++) {
			EpsilonRange_ListElement listElement = (EpsilonRange_ListElement) epsilonRange_list
					.get(i);
			DataObject neighbourhood_object = listElement.getDataObject();
			if (!neighbourhood_object.isProcessed()) {
				new_r_dist = Math.max(coreDistance, listElement.getDistance());
				seeds.add(new_r_dist, neighbourhood_object,
						neighbourhood_object.getKey());
			}
		}
	}

	/**
	 * Classifies a given instance.
	 * 
	 * @param instance
	 *            The instance to be assigned to a cluster
	 * @return int The number of the assigned cluster as an integer
	 * @throws java.lang.Exception
	 *             If instance could not be clustered successfully
	 */
	public int clusterInstance(Instance instance) throws Exception {
		throw new Exception();
	}

	/**
	 * Returns the number of clusters.
	 * 
	 * @return int The number of clusters generated for a training dataset.
	 * @throws java.lang.Exception
	 *             If number of clusters could not be returned successfully
	 */
	public int numberOfClusters() throws Exception {
		return numberOfGeneratedClusters;
	}

	/**
	 * Returns an enumeration of all the available options.
	 * 
	 * @return Enumeration An enumeration of all available options.
	 */
	public Enumeration listOptions() {
		Vector vector = new Vector();

		vector.addElement(new Option("\tepsilon (default = 0.9)", "E", 1,
				"-E <double>"));

		vector.addElement(new Option("\tminPoints (default = 6)", "M", 1,
				"-M <int>"));

		vector.addElement(new Option(
				"\tindex (database) used for OPTICS (default = weka.clusterers.forOPTICSAndDBScan.Databases.SequentialDatabase)",
				"I", 1, "-I <String>"));

		vector.addElement(new Option(
				"\tdistance-type (default = weka.clusterers.forOPTICSAndDBScan.DataObjects.EuclidianDataObject)",
				"D", 1, "-D <String>"));

		vector.addElement(new Option(
				"\twrite results to OPTICS_#TimeStamp#.TXT - File", "F", 0,
				"-F"));

		vector.addElement(new Option(
				"\tsuppress the display of the GUI after building the clusterer",
				"no-gui", 0, "-no-gui"));

		vector.addElement(new Option(
				"\tThe file to save the generated database to. If a directory\n"
						+ "\tis provided, the database doesn't get saved.\n"
						+ "\tThe generated file can be viewed with the OPTICS Visualizer:\n"
						+ "\t  java " + OPTICS_Visualizer.class.getName()
						+ " [file.ser]\n" + "\t(default: .)", "db-output", 1,
				"-db-output <file>"));

		return vector.elements();
	}

	/**
	 * Sets the OptionHandler's options using the given list. All options will
	 * be set (or reset) during this call (i.e. incremental setting of options
	 * is not possible).
	 * <p/>
	 * 
	 * <!-- options-start --> Valid options are:
	 * <p/>
	 * 
	 * <pre>
	 * -E &lt;double&gt;
	 *  epsilon (default = 0.9)
	 * </pre>
	 * 
	 * <pre>
	 * -M &lt;int&gt;
	 *  minPoints (default = 6)
	 * </pre>
	 * 
	 * <pre>
	 * -I &lt;String&gt;
	 *  index (database) used for OPTICS (default = weka.clusterers.forOPTICSAndDBScan.Databases.SequentialDatabase)
	 * </pre>
	 * 
	 * <pre>
	 * -D &lt;String&gt;
	 *  distance-type (default = weka.clusterers.forOPTICSAndDBScan.DataObjects.EuclidianDataObject)
	 * </pre>
	 * 
	 * <pre>
	 * -F
	 *  write results to OPTICS_#TimeStamp#.TXT - File
	 * </pre>
	 * 
	 * <pre>
	 * -no-gui
	 *  suppress the display of the GUI after building the clusterer
	 * </pre>
	 * 
	 * <pre>
	 * -db-output &lt;file&gt;
	 *  The file to save the generated database to. If a directory
	 *  is provided, the database doesn't get saved.
	 *  The generated file can be viewed with the OPTICS Visualizer:
	 *    java weka.clusterers.forOPTICSAndDBScan.OPTICS_GUI.OPTICS_Visualizer [file.ser]
	 *  (default: .)
	 * </pre>
	 * 
	 * <!-- options-end -->
	 * 
	 * @param options
	 *            The list of options as an array of strings
	 * @throws java.lang.Exception
	 *             If an option is not supported
	 */
	public void setOptions(String[] options) throws Exception {
		String optionString = Utils.getOption('E', options);
		if (optionString.length() != 0)
			setEpsilon(Double.parseDouble(optionString));
		else
			setEpsilon(0.9);

		optionString = Utils.getOption('M', options);
		if (optionString.length() != 0)
			setMinPoints(Integer.parseInt(optionString));
		else
			setMinPoints(6);

		optionString = Utils.getOption('I', options);
		if (optionString.length() != 0)
			setDatabase_Type(optionString);
		else
			setDatabase_Type(weka.clusterers.forOPTICSAndDBScan.Databases.SequentialDatabase.class
					.getName());

		optionString = Utils.getOption('D', options);
		if (optionString.length() != 0)
			setDatabase_distanceType(optionString);
		else
			setDatabase_distanceType(weka.clusterers.forOPTICSAndDBScan.DataObjects.EuclidianDataObject.class
					.getName());

		setWriteOPTICSresults(Utils.getFlag('F', options));

		setShowGUI(!Utils.getFlag("no-gui", options));

		optionString = Utils.getOption("db-output", options);
		if (optionString.length() != 0)
			setDatabaseOutput(new File(optionString));
		else
			setDatabaseOutput(new File("."));
	}

	/**
	 * Gets the current option settings for the OptionHandler.
	 * 
	 * @return String[] The list of current option settings as an array of
	 *         strings
	 */
	public String[] getOptions() {
		Vector<String> result;

		result = new Vector<String>();

		result.add("-E");
		result.add("" + getEpsilon());

		result.add("-M");
		result.add("" + getMinPoints());

		result.add("-I");
		result.add("" + getDatabase_Type());

		result.add("-D");
		result.add("" + getDatabase_distanceType());

		if (getWriteOPTICSresults())
			result.add("-F");

		if (!getShowGUI())
			result.add("-no-gui");

		result.add("-db-output");
		result.add("" + getDatabaseOutput());

		return result.toArray(new String[result.size()]);
	}

	/**
	 * Returns a new Class-Instance of the specified database
	 * 
	 * @param database_Type
	 *            String of the specified database
	 * @param instances
	 *            Instances that were delivered from WEKA
	 * @return Database New constructed Database
	 */
	public Database databaseForName(String database_Type, Instances instances) {
		Object o = null;

		Constructor co = null;
		try {
			co = (Class.forName(database_Type))
					.getConstructor(new Class[] { Instances.class });
			o = co.newInstance(new Object[] { instances });
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}

		return (Database) o;
	}

	/**
	 * Returns a new Class-Instance of the specified database
	 * 
	 * @param database_distanceType
	 *            String of the specified distance-type
	 * @param instance
	 *            The original instance that needs to hold by this DataObject
	 * @param key
	 *            Key for this DataObject
	 * @param database
	 *            Link to the database
	 * @return DataObject New constructed DataObject
	 */
	public DataObject dataObjectForName(String database_distanceType,
			Instance instance, String key, Database database) {
		Object o = null;

		Constructor co = null;
		try {
			co = (Class.forName(database_distanceType))
					.getConstructor(new Class[] { Instance.class, String.class,
							Database.class });
			o = co.newInstance(new Object[] { instance, key, database });
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}

		return (DataObject) o;
	}

	/**
	 * Sets a new value for minPoints
	 * 
	 * @param minPoints
	 *            MinPoints
	 */
	public void setMinPoints(int minPoints) {
		this.minPoints = minPoints;
	}

	/**
	 * Sets a new value for epsilon
	 * 
	 * @param epsilon
	 *            Epsilon
	 */
	public void setEpsilon(double epsilon) {
		this.epsilon = epsilon;
	}

	/**
	 * Returns the value of epsilon
	 * 
	 * @return double Epsilon
	 */
	public double getEpsilon() {
		return epsilon;
	}

	/**
	 * Returns the value of minPoints
	 * 
	 * @return int MinPoints
	 */
	public int getMinPoints() {
		return minPoints;
	}

	/**
	 * Returns the distance-type
	 * 
	 * @return String Distance-type
	 */
	public String getDatabase_distanceType() {
		return database_distanceType;
	}

	/**
	 * Returns the type of the used index (database)
	 * 
	 * @return String Index-type
	 */
	public String getDatabase_Type() {
		return database_Type;
	}

	/**
	 * Sets a new distance-type
	 * 
	 * @param database_distanceType
	 *            The new distance-type
	 */
	public void setDatabase_distanceType(String database_distanceType) {
		this.database_distanceType = database_distanceType;
	}

	/**
	 * Sets a new database-type
	 * 
	 * @param database_Type
	 *            The new database-type
	 */
	public void setDatabase_Type(String database_Type) {
		this.database_Type = database_Type;
	}

	/**
	 * Returns the flag for writing actions
	 * 
	 * @return writeOPTICSresults (flag)
	 */
	public boolean getWriteOPTICSresults() {
		return writeOPTICSresults;
	}

	/**
	 * Sets the flag for writing actions
	 * 
	 * @param writeOPTICSresults
	 *            Results are written to a file if the flag is set
	 */
	public void setWriteOPTICSresults(boolean writeOPTICSresults) {
		this.writeOPTICSresults = writeOPTICSresults;
	}

	/**
	 * Returns the flag for showing the OPTICS visualizer GUI.
	 * 
	 * @return true if the GUI is displayed
	 */
	public boolean getShowGUI() {
		return showGUI;
	}

	/**
	 * Sets the flag for displaying the GUI.
	 * 
	 * @param value
	 *            if true, then the OPTICS visualizer GUI will be displayed
	 *            after building the clusterer
	 */
	public void setShowGUI(boolean value) {
		showGUI = value;
	}

	/**
	 * Returns the file to save the database to - if directory, database is not
	 * saved.
	 * 
	 * @return the file to save the database to a directory if saving is ignored
	 */
	public File getDatabaseOutput() {
		return databaseOutput;
	}

	/**
	 * Sets the the file to save the generated database to. If a directory is
	 * provided, the datbase doesn't get saved.
	 * 
	 * @param value
	 *            the file to save the database to or a directory if saving is
	 *            to be ignored
	 */
	public void setDatabaseOutput(File value) {
		databaseOutput = value;
	}

	/**
	 * Returns the resultVector
	 * 
	 * @return resultVector
	 */
	public FastVector getResultVector() {
		return resultVector;
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String epsilonTipText() {
		return "radius of the epsilon-range-queries";
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String minPointsTipText() {
		return "minimun number of DataObjects required in an epsilon-range-query";
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String database_TypeTipText() {
		return "used database";
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String database_distanceTypeTipText() {
		return "used distance-type";
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String writeOPTICSresultsTipText() {
		return "if the -F option is set, the results are written to OPTICS_#TimeStamp#.TXT";
	}

	/**
	 * Returns the tip text for this property.
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String showGUITipText() {
		return "Defines whether the OPTICS Visualizer is displayed after the clusterer has been built or not.";
	}

	/**
	 * Returns the tip text for this property.
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String databaseOutputTipText() {
		return "The optional output file for the generated database object - can "
				+ "be viewed with the OPTICS Visualizer.\n"
				+ "java "
				+ OPTICS_Visualizer.class.getName() + " [file.ser]";
	}

	/**
	 * Returns a string describing this DataMining-Algorithm
	 * 
	 * @return String Information for the gui-explorer
	 */
	public String globalInfo() {
		return getTechnicalInformation().toString();
	}

	/**
	 * Returns an instance of a TechnicalInformation object, containing detailed
	 * information about the technical background of this class, e.g., paper
	 * reference or book this class is based on.
	 * 
	 * @return the technical information about this class
	 */
	public TechnicalInformation getTechnicalInformation() {
		TechnicalInformation result;

		result = new TechnicalInformation(Type.INPROCEEDINGS);
		result.setValue(Field.AUTHOR,
				"Mihael Ankerst and Markus M. Breunig and Hans-Peter Kriegel and Joerg Sander");
		result.setValue(Field.TITLE,
				"OPTICS: Ordering Points To Identify the Clustering Structure");
		result.setValue(Field.BOOKTITLE,
				"ACM SIGMOD International Conference on Management of Data");
		result.setValue(Field.YEAR, "1999");
		result.setValue(Field.PAGES, "49-60");
		result.setValue(Field.PUBLISHER, "ACM Press");

		return result;
	}

	/**
	 * Returns the internal database
	 * 
	 * @return the internal database
	 */
	public SERObject getSERObject() {
		SERObject serObject = new SERObject(resultVector, database.size(),
				database.getInstances().numAttributes(), getEpsilon(),
				getMinPoints(), writeOPTICSresults, getDatabase_Type(),
				getDatabase_distanceType(), numberOfGeneratedClusters,
				Utils.doubleToString(elapsedTime, 3, 3));
		return serObject;
	}

	/**
	 * Returns a description of the clusterer
	 * 
	 * @return the clusterer as string
	 */
	public String toString() {
		StringBuffer stringBuffer = new StringBuffer();
		stringBuffer
				.append("OPTICS clustering results\n"
						+ "============================================================================================\n\n");
		stringBuffer.append("Clustered DataObjects: " + database.size() + "\n");
		stringBuffer.append("Number of attributes: "
				+ database.getInstances().numAttributes() + "\n");
		stringBuffer.append("Epsilon: " + getEpsilon() + "; minPoints: "
				+ getMinPoints() + "\n");
		stringBuffer.append("Write results to file: "
				+ (writeOPTICSresults ? "yes" : "no") + "\n");
		stringBuffer.append("Index: " + getDatabase_Type() + "\n");
		stringBuffer.append("Distance-type: " + getDatabase_distanceType()
				+ "\n");
		stringBuffer.append("Number of generated clusters: "
				+ numberOfGeneratedClusters + "\n");
		DecimalFormat decimalFormat = new DecimalFormat(".##");
		stringBuffer.append("Elapsed time: "
				+ decimalFormat.format(elapsedTime) + "\n\n");

		for (int i = 0; i < resultVector.size(); i++) {
			stringBuffer.append(format_dataObject((DataObject) resultVector
					.elementAt(i)));
		}
		return stringBuffer.toString() + "\n";
	}

	/**
	 * Returns the revision string.
	 * 
	 * @return the revision
	 */
	public String getRevision() {
		return RevisionUtils.extract("$Revision: 5538 $");
	}

	/**
	 * Main Method for testing OPTICS
	 * 
	 * @param args
	 *            Valid parameters are: 'E' epsilon (default = 0.9); 'M'
	 *            minPoints (default = 6); 'I' index-type (default =
	 *            weka.clusterers
	 *            .forOPTICSAndDBScan.Databases.SequentialDatabase); 'D'
	 *            distance-type (default =
	 *            weka.clusterers.forOPTICSAndDBScan.DataObjects
	 *            .EuclidianDataObject); 'F' write results to
	 *            OPTICS_#TimeStamp#.TXT - File
	 */
	public static void main(String[] args) {
		runClusterer(new OPTICS(), args);
	}
}
